<?php
/**
 * Http响应封装
 * 
 * 从v1.1.1开始，IHttpResponse会自动对gzip或deflate编码过的内容进行解码
 *
 * @version $Id$
 * @package if
 * @subpackage util.net
 * @since 1.0
 */
class IHttpResponse extends IObject {
	private $fullText;
	private $headers = array();
	private $status;
	private $httpVersion;
	private $body;
	private $decoded = false;
	
	/**
	 * 构造器
	 * 
	 * 如果同一个字段中有多个值，那么该字段的值为一个数组(v1.1.0)
	 *
	 * @param string $responseString 响应字符串
	 * @since 1.0
	 */
	function __construct($responseString) {
		$this->fullText = $responseString;
		$pieces = preg_split("/(\r?\n){2}/s", $responseString, 2);
		$header = $pieces[0];
		$this->body = isset($pieces[1]) ? $pieces[1] : "";
		$lines = preg_split("/\r?\n/s", $header);
		foreach ($lines as $index=>$line) {
			$match = array();
			if (preg_match("/^HTTP\\/(1\\.\\d) (\\d+) /", $line, $match)) {
				$this->httpVersion = $match[1];
				$this->status = $match[2];
			}
			if (strstr($line, ":")) {
				list($headerName, $headerValue) = explode(":", $line, 2);
				$headerValue = trim($headerValue);
				$headerName = strtolower($headerName);
				if (!isset($this->headers[$headerName])) {
					$this->headers[$headerName] = $headerValue;
				}
				else {
					if (is_array($this->headers[$headerName])) {
						$this->headers[$headerName][] = $headerValue;
					}
					else {
						$this->headers[$headerName] = array(
							$this->headers[$headerName],
							$headerValue
						);
					}
				}
			}
		}
	}
	
	/**
	 * 取得响应的全文
	 *
	 * 包含报头
	 * 
	 * @return string
	 * @since 1.0
	 */
	function fullText() {
		return $this->fullText;
	}
	
	/**
	 * 取得Header信息
	 *
	 * @return array
	 * @since 1.0
	 */
	function headers() {
		return $this->headers;
	}
	
	/**
	 * 取得单个Header信息
	 *
	 * @param string $name 名称
	 * @return string
	 * @since 1.0
	 */
	function header($name) {
		if (isset($this->headers[$name])) {
			return $this->headers[$name];
		}
		return null;
	}
	
	/**
	 * 取得状态码
	 * 
	 * 200, 301 ...
	 *
	 * @return integer
	 * @since 1.0
	 */
	function status() {
		return $this->status;
	}
	
	/**
	 * 取得主体
	 *
	 * @param boolean $decoded 是否是解码后的结果，v1.1.1加入
	 * @return string
	 * @since 1.0
	 */
	function body($decoded = true) {
		if ($this->decoded || !$decoded) {
			return $this->body;
		}
		
		//读取chunked内容
		if (strtolower($this->header("transfer-encoding")) == "chunked") {
			$this->body = $this->_readChunked($this->body);
		}
		
		//content decoding v1.1.1
		switch (strtolower($this->header("content-encoding"))) {
            case "gzip": 
                $this->body = $this->_decodeGzip($this->body);
                break;
            case "deflate":
                $this->body = $this->_decodeDeflate($this->body);
                break;
		}
		 $this->decoded = true;
		return $this->body;
	}
	
	/**
	 * 取得Http版本
	 *
	 * @return float
	 * @since 1.0
	 */
	function httpVersion() {
		return $this->httpVersion;
	}
	
	/**
	 * 取得所有cookie
	 * 
	 * 返回的结果是包含一组ICookie对象的数组
	 *
	 * @param boolean $alive 是否只包含有效的cookie
	 * @return array
	 * @since 1.1.0
	 */
	function cookies($alive = true) {
		if (!isset($this->headers["set-cookie"])) {
			return array();
		}
		$cookies = array();
		if (!is_array($this->headers["set-cookie"])) {
			$this->headers["set-cookie"] = array($this->headers["set-cookie"]);
		}
		foreach ($this->headers["set-cookie"] as $str) {
			$cookie = ICookie::parse($str);
			if (!$alive || $cookie->maxAge() >= 0) {
				$cookies[] = $cookie;
			}
		}
		return $cookies;
	}
	
	/**
	 * 设置相应的主体内容
	 *
	 * @param string $body 主体内容
	 * @param boolean $decoded 是否已经解码
	 * @since 1.1.1
	 */
	function setBody($body, $decoded = true) {
		$this->body = $body;
		$this->decoded = $decoded;
	}
	
	/**
	* Decodes the message-body encoded by gzip
    * 
    * 该方法修改自PEAR Package: HTTP_Request2
    *
    * The real decoding work is done by gzinflate() built-in function, this
    * method only parses the header and checks data for compliance with
    * RFC 1952
    *
    * @param    string  gzip-encoded data
    * @return   string  decoded data
    * @throws   INetException
    * @link     http://tools.ietf.org/html/rfc1952
    * @since 1.1.1
    */
    private function _decodeGzip($data) { 
    	$length = strlen($data);
        // If it doesn't look like gzip-encoded data, don't bother
        if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
            return $data;
        }
        if (!function_exists('gzinflate')) {
            throw new INetException('Unable to decode body: gzip extension not available', 0x100111);
        }
        $method = ord(substr($data, 2, 1));
        if (8 != $method) {
            throw new INetException('Error parsing gzip header: unknown compression method', 0x100112);
        }
        $flags = ord(substr($data, 3, 1));
        if ($flags & 224) {
            throw new INetException('Error parsing gzip header: reserved bits are set', 0x100113);
        }

        // header is 10 bytes minimum. may be longer, though.
        $headerLength = 10;
        // extra fields, need to skip 'em
        if ($flags & 4) {
            if ($length - $headerLength - 2 < 8) {
                throw new INetException('Error parsing gzip header: data too short', 0x100114);
            }
            $extraLength = unpack('v', substr($data, 10, 2));
            if ($length - $headerLength - 2 - $extraLength[1] < 8) {
                throw new INetException('Error parsing gzip header: data too short', 0x100115);
            }
            $headerLength += $extraLength[1] + 2;
        }
        // file name, need to skip that
        if ($flags & 8) {
            if ($length - $headerLength - 1 < 8) {
                throw new INetException('Error parsing gzip header: data too short', 0x100116);
            }
            $filenameLength = strpos(substr($data, $headerLength), chr(0));
            if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
                throw new INetException('Error parsing gzip header: data too short', 0x100117);
            }
            $headerLength += $filenameLength + 1;
        }
        // comment, need to skip that also
        if ($flags & 16) {
            if ($length - $headerLength - 1 < 8) {
                throw new INetException('Error parsing gzip header: data too short', 0x100118);
            }
            $commentLength = strpos(substr($data, $headerLength), chr(0));
            if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
                throw new INetException('Error parsing gzip header: data too short', 0x100119);
            }
            $headerLength += $commentLength + 1;
        }
        // have a CRC for header. let's check
        if ($flags & 2) {
            if ($length - $headerLength - 2 < 8) {
                throw new INetException('Error parsing gzip header: data too short', 0x100120);
            }
            $crcReal   = 0xffff & crc32(substr($data, 0, $headerLength));
            $crcStored = unpack('v', substr($data, $headerLength, 2));
            if ($crcReal != $crcStored[1]) {
                throw new INetException('Header CRC check failed', 0x100121);
            }
            $headerLength += 2;
        }
        // unpacked data CRC and size at the end of encoded data
        $tmp = unpack('V2', substr($data, -8));
        $dataCrc  = $tmp[1];
        $dataSize = $tmp[2];

        // finally, call the gzinflate() function
        // don't pass $dataSize to gzinflate, see bugs #13135, #14370
        $unpacked = gzinflate(substr($data, $headerLength, -8));
        if (false === $unpacked) {
            throw new INetException('gzinflate() call failed', 0x100122);
        } elseif ($dataSize != strlen($unpacked)) {
            throw new INetException('Data size check failed', 0x100123);
        } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
            throw new INetException('Data CRC check failed', 0x100124);
        }
        return $unpacked;
    }

   /**
    * Decodes the message-body encoded by deflate
    * 
    * 该方法修改自PEAR Package: HTTP_Request2
    *
    * @param    string  deflate-encoded data
    * @return   string  decoded data
    * @throws   INetException
    * @since 1.1.1
    */
    private function _decodeDeflate($data) {
        if (!function_exists('gzuncompress')) {
            throw new INetException('Unable to decode body: gzip extension not available', 0x100125);
        }
        // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950,
        // while many applications send raw deflate stream from RFC 1951.
        // We should check for presence of zlib header and use gzuncompress() or
        // gzinflate() as needed. See bug #15305
        $header = unpack('n', substr($data, 0, 2));
        return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data);
    }
    
    /**
     * 将当前对象转化为字符串
     *
     * @return string
     * @since 1.1.1
     */
    function __toString() {
    	return $this->body();
    }
    
    /**
     * 从响应中获取XML对象
     *
     * @return IXmlNode
     * @since 1.1.1
     */
    function xml() {
    	return new IXmlNode($this->body(), null, true);
    }
    
    private function _readChunked($body) {
    	if (preg_match("/^\s*([0-9a-f]+)\s+\r?\n/i", $body, $match)) {
			$idlength = strlen($match[0]);
    		$length = hexdec($match[1]);
    		if ($length == 0) {
    			return  "";
    		}
    		$realBody = substr($body, $idlength, $length);
    		$left = substr($body, $idlength + $length);
    		if ($left) {
    			$realBody .= $this->_readChunked($left);
    		}
    		
    		return $realBody;
    	}
    	return "";
    }
    
    /**
     * 获取响应的长度
     *
     * @return integer
     * @since 1.1.2
     */
    function length() {
    	return strlen($this->body());
    }
}

?>